]> dgit.raspbian.org Git - gnome-shell.git/commitdiff
slider: Add mark snapping support
authorrevisto <therevisto@gmail.com>
Mon, 22 Dec 2025 21:32:32 +0000 (01:02 +0330)
committerMarge Bot <marge-bot@gnome.org>
Tue, 3 Feb 2026 13:11:37 +0000 (13:11 +0000)
Add API to add and clear marks, similar to GtkScale. The
slider value will snap to a mark when close to it, making
it easy for the user to position the slider exactly at
the mark's value.

The unsnapped value is still tracked internally, to allow
escaping snap zones when the scroll step is smaller than
the snap threshold.

Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/4004>

js/ui/slider.js

index 01bb7c8f88145b9a55e6f26dc15125861788cac8..563d4148c28cadc8db9142af72e97e194d232e33 100644 (file)
@@ -5,6 +5,7 @@ import GObject from 'gi://GObject';
 import * as BarLevel from './barLevel.js';
 
 const SLIDER_SCROLL_STEP = 0.02; /* Slider scrolling step in % */
+const SNAP_THRESHOLD = 0.04; /* Snap to marks within 4% */
 
 export const Slider = GObject.registerClass({
     Signals: {
@@ -35,6 +36,25 @@ export const Slider = GObject.registerClass({
         this.add_action(this._panGesture);
 
         this._customAccessible.connect('get-minimum-increment', this._getMinimumIncrement.bind(this));
+
+        this._marks = new Set();
+        this._unsnappedValue = null;
+    }
+
+    addMark(value) {
+        this._marks.add(value);
+    }
+
+    clearMarks() {
+        this._marks.clear();
+    }
+
+    _snapToMark(value) {
+        for (const mark of this._marks) {
+            if (Math.abs(value - mark) < SNAP_THRESHOLD)
+                return mark;
+        }
+        return value;
     }
 
     vfunc_style_changed() {
@@ -111,16 +131,19 @@ export const Slider = GObject.registerClass({
         this._moveHandle(coords.x, coords.y);
     }
 
-    step(nSteps) {
-        const delta = nSteps * SLIDER_SCROLL_STEP;
-        const value = Math.min(Math.max(0, this._value + delta), this._maxValue);
-
-        if (value !== this.value) {
-            this.value = value;
-            return true;
-        }
+    _applyDelta(delta) {
+        // Track unsnapped value to allow escaping snap zones when scrolling/using arrow keys.
+        // Without this, if the scroll step is smaller than the snap threshold,
+        // the slider gets stuck at the mark because each scroll snaps back.
+        const base = this._unsnappedValue ?? this._value;
+        const oldValue = this._value;
+        this._unsnappedValue = Math.clamp(base + delta, 0, this._maxValue);
+        this.value = this._snapToMark(this._unsnappedValue);
+        return this._value !== oldValue;
+    }
 
-        return false;
+    step(nSteps) {
+        return this._applyDelta(nSteps * SLIDER_SCROLL_STEP);
     }
 
     vfunc_scroll_event(event) {
@@ -155,7 +178,7 @@ export const Slider = GObject.registerClass({
             const rtl = this.get_text_direction() === Clutter.TextDirection.RTL;
             const increaseKey = rtl ? Clutter.KEY_Left : Clutter.KEY_Right;
             const delta = key === increaseKey ? 0.1 : -0.1;
-            this.value = Math.max(0, Math.min(this._value + delta, this._maxValue));
+            this._applyDelta(delta);
             return Clutter.EVENT_STOP;
         }
         return super.vfunc_key_press_event(event);
@@ -176,7 +199,8 @@ export const Slider = GObject.registerClass({
             newvalue = 1;
         else
             newvalue = (relX - this._handleRadius) / (width - 2 * this._handleRadius);
-        this.value = newvalue * this._maxValue;
+        this._unsnappedValue = newvalue * this._maxValue;
+        this.value = this._snapToMark(this._unsnappedValue);
     }
 
     _getMinimumIncrement() {